#include "jaspconfiguration.h"
#include "jaspconfigurationparser.h"
#include "utilities/settings.h"
#include "gui/preferencesmodel.h"
#include "log.h"
#include <QNetworkReply>
#include <QStandardPaths>
#include <QFile>
#include <QTextStream>
#include <QRegularExpression>
#include <QString>
#include <QDir>
#include <QMetaType>

JASPConfiguration* JASPConfiguration::_instance = nullptr;

JASPConfiguration* JASPConfiguration::getInstance(QObject *parent)
{
	if(!_instance)
		_instance = new JASPConfiguration(parent);
	return _instance;
}


JASPConfiguration::JASPConfiguration(QObject *parent)
	: QObject{parent}
{
}

bool JASPConfiguration::constantExists(const QString &constant, const QString &module, const QString &analysis)
{
	return _definedConstants.contains(module) && _definedConstants[module].contains(analysis) && _definedConstants[module][analysis].contains(constant);
}

QVariant JASPConfiguration::getConstant(const QString &constant, const QVariant& defaultValue, const QString &module, const QString &analysis)
{
    if(constantExists(constant, module, analysis))
		return _definedConstants[module][analysis][constant];
    else if(constantExists(constant, module))
		return _definedConstants[module][""][constant];
    else if(constantExists(constant))
		return _definedConstants[""][""][constant];
    return defaultValue;
}

bool JASPConfiguration::optionSet(const QString &module, const QString &analysis, const QString &optionName)
{
	return _analysisOptions.contains(module) && _analysisOptions[module].contains(analysis) && _analysisOptions[module][analysis].isMember(optionName.toStdString());
}

bool JASPConfiguration::optionsSet(const QString &module, const QString &analysis)
{
	return _analysisOptions.contains(module) && _analysisOptions[module].contains(analysis);
}

bool JASPConfiguration::optionLocked(const QString &module, const QString &analysis, const QString &optionName)
{
	return _analysisOptionsLocked[module][analysis][optionName];
}

Json::Value JASPConfiguration::getAnalysisOptionValues(const QString &module, const QString &analysis)
{
	if(optionsSet(module, analysis))
		return _analysisOptions[module][analysis];
	return Json::nullValue;
}

void JASPConfiguration::processConfiguration()
{
	clear();
	if(Settings::value(Settings::USE_CONFIGURATION_FILE).toBool()) {
		bool localOK = processLocal();

		//read, parse & save remote settings
		if(Settings::value(Settings::REMOTE_CONFIGURATION).toBool())
		{
			auto conn = std::make_shared<QMetaObject::Connection>();
			*conn = connect(&_networkManager, &QNetworkAccessManager::finished, this, [this, conn, localOK](QNetworkReply* reply) {
				QObject::disconnect(*conn);
				reply->deleteLater();

				try
				{
					if(reply->error())
						throw std::runtime_error("Error fetching remote configuration file " + reply->request().url().toString().toStdString() + " : " + reply->errorString().toStdString());
					QByteArray payload = reply->readAll();
					clear();
					if(!JASPConfigurationParser::getParser(JASPConfigurationParser::Format::TOML)->parse(this, payload))
						throw std::runtime_error("Parsing failed");

					auto conf = getDefaultConfFile(true);
					conf->write(payload);
					conf->close();
					Log::log() << "Stored local copy of remote configuration" << std::endl;
					emit this->configurationProcessed(configState::REMOTE);
				}
				catch (std::runtime_error& e)
				{
					Log::log() << "Failed to process remote configuration: " << e.what() << std::endl;
					if(!localOK)
						emit this->configurationProcessed(configState::FAIL);
					else
						emit this->configurationProcessed(configState::LOCAL);
					return;
				}
			});

			//make the request
			QNetworkRequest request(Settings::value(Settings::REMOTE_CONFIGURATION_URL).toString());
			QNetworkReply* reply = _networkManager.get(request);
			connect(reply, &QNetworkReply::sslErrors, this, &JASPConfiguration::sslErrors);
		}
		else
			emit configurationProcessed(configState::LOCAL);
	}
}

bool JASPConfiguration::processLocal()
{
	try
	{
		auto localConfFile = getLocalConfFile();
		QTextStream in(localConfFile.get());
        if(!JASPConfigurationParser::getParser(JASPConfigurationParser::Format::TOML)->parse(this, in.readAll()))
			throw std::runtime_error("Parsing failed");
	}
	catch(std::runtime_error& e)
	{
		Log::log() <<  "Could not parse local configuration: " << e.what() << std::endl;
		return false;
	}
	return true;
}

void JASPConfiguration::clear()
{
	_definedConstants.clear();
	_analysisOptions.clear();
	_modulesToLoad.clear();
}

void JASPConfiguration::remoteChanged()
{
	processConfiguration();
}

std::shared_ptr<QFile> JASPConfiguration::getLocalConfFile(bool truncate) {

	//the right default path can only be determined here, so after jasp init
	if(PreferencesModel::prefs()->localConfigurationPATH() == "")
	{
		defaultConfigurationFolder = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
		defaultConfigurationPath = defaultConfigurationFolder + "/" + defaultConfigurationFilename;
		PreferencesModel::prefs()->setLocalConfigurationPATH(defaultConfigurationPath);
	}

	if(Settings::value(Settings::REMOTE_CONFIGURATION).toBool()) //if we use a remote but we are offline
		return getDefaultConfFile(truncate);
	else //user specified local file
		return getConfFile(Settings::value(Settings::LOCAL_CONFIGURATION_PATH).toString(), truncate);
}

std::shared_ptr<QFile> JASPConfiguration::getDefaultConfFile(bool truncate)
{
	QDir confDir;
	defaultConfigurationFolder = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
	defaultConfigurationPath = defaultConfigurationFolder + "/" + defaultConfigurationFilename;
	if(!confDir.mkpath(defaultConfigurationFolder))
		throw std::runtime_error("Could not access app configuration path");

	return getConfFile(defaultConfigurationPath, truncate);
}

std::shared_ptr<QFile> JASPConfiguration::getConfFile(QString path, bool truncate)
{
	std::shared_ptr<QFile> localConfFile = std::make_shared<QFile>(path);
	QIODeviceBase::OpenMode flags = QIODeviceBase::ReadWrite | QIODeviceBase::Text | (truncate ? QIODeviceBase::Truncate : QIODeviceBase::NotOpen);
	if (!localConfFile->open(flags))
		throw std::runtime_error("Could not open local configuration file " + path.toStdString() + ": " + localConfFile->errorString().toStdString());
	return localConfFile;
}

bool JASPConfiguration::addConstant(const QString& key, const QVariant& value, const QString& moduleName, const QString& analysisName)
{
	_definedConstants[moduleName][analysisName][key] = value;
	return true;
}

bool JASPConfiguration::addOption(const QString& key, const QVariant& value, bool locked, const QString& moduleName, const QString& analysisName)
{
	_analysisOptionsLocked[moduleName][analysisName][key] = locked;
	if(!(_analysisOptions.contains(moduleName) && _analysisOptions[moduleName].contains(analysisName)))
		_analysisOptions[moduleName][analysisName] = Json::Value(Json::objectValue);

	Json::Value & option = _analysisOptions[moduleName][analysisName][key.toStdString()];
	
	switch(value.userType())
	{
	case QMetaType::Double:			option = value.value<double>();								break;
	case QMetaType::LongLong:		option = static_cast<int64_t>(value.value<long long>());	break;
	case QMetaType::Bool:			option = value.value<bool>();								break;
	default:					
		if(value.canConvert<QString>()) 
									option = value.toString().toStdString();
		break;
	}
	
	return true;
}

void JASPConfiguration::sslErrors(const QList<QSslError> &errors)
{
	for (const QSslError &error : errors)
		Log::log() << "Error fetching remote settings:" << error.errorString().toStdString() << std::endl;
}



